package shortlink.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import shortlink.common.convention.exception.ServiceException;
import shortlink.common.database.BaseDO;
import shortlink.common.enums.VailDateTypeEnum;
import shortlink.dao.entity.LinkgotoDO;
import shortlink.dao.entity.ShortLinkDO;
import shortlink.dao.mapper.LinkgotoMapper;
import shortlink.dao.mapper.ShortLinkMapper;
import shortlink.dto.biz.ShortLinkStatsRecordDTO;
import shortlink.dto.req.ShortLinkBatchCreateReqDTO;
import shortlink.dto.req.ShortLinkCreateReqDTO;
import shortlink.dto.req.ShortLinkPageReqDTO;
import shortlink.dto.req.ShortLinkUpdateReqDTO;
import shortlink.dto.resp.*;
import shortlink.mq.producer.ShortLinkStatsSaveRoketmqProducer;
import shortlink.service.ShortLinkService;
import shortlink.toolkit.HashUtil;
import shortlink.toolkit.LinkUtil;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import lombok.extern.slf4j.Slf4j;

import static shortlink.common.constant.RedisKeyConstant.*;

@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    @Value("${short-link.domain.default}")
    private String defaultDomain;
    private final LinkgotoMapper linkgotoMapper;
    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final ShortLinkMapper shortLinkMapper;
    private final ShortLinkStatsSaveRoketmqProducer shortLinkStatsSaveProducer;
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String shortlinkSuffix = generator(requestParam);
        String fullshortUrl= StrBuilder.create(defaultDomain)
                .append("/")
                .append(shortlinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .domain(defaultDomain)
                .createType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortlinkSuffix)
                .enableStatus(0)
                .delTime(0L)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .fullShortUrl(fullshortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        LinkgotoDO linkgotoDO = LinkgotoDO.builder()
                .fullShortUrl(fullshortUrl)
                .gid(requestParam.getGid())
                .build();
        try{
            baseMapper.insert(shortLinkDO);
            linkgotoMapper.insert(linkgotoDO);
        }catch (DuplicateKeyException e){
            if(!shortUriCreateCachePenetrationBloomFilter.contains(fullshortUrl)){
                shortUriCreateCachePenetrationBloomFilter.add(fullshortUrl);
            }
            throw new ServiceException(String.format("短链接：%s 生成重复", fullshortUrl));
        }

        //缓存预热
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY,fullshortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
        );

        shortUriCreateCachePenetrationBloomFilter.add(fullshortUrl);

        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(fullshortUrl)
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .build();


    }

    @Override
    public ShortLinkBatchCreateRespDTO batchcreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes=requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result=new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}",originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasshortLinkDO = baseMapper.selectOne(queryWrapper);
        if(hasshortLinkDO == null){
            throw new ServiceException("短链接记录不存在");
        }
        if(Objects.equals(hasshortLinkDO.getGid(),requestParam.getGid())){
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()),ShortLinkDO::getValidDate,null);
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .shortUri(hasshortLinkDO.getShortUri())
                    .createType(hasshortLinkDO.getCreateType())
                    .domain(hasshortLinkDO.getDomain())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .favicon(Objects.equals(hasshortLinkDO.getOriginUrl(), requestParam.getOriginUrl()) ? hasshortLinkDO.getFavicon() : getFavicon(requestParam.getOriginUrl()))
                    .build();
            baseMapper.update(shortLinkDO,updateWrapper);
        }else {
            //修改分组需要加上读写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rlock = readWriteLock.writeLock();
            rlock.lock();
            try {

                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasshortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();

                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(defaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createType(hasshortLinkDO.getCreateType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasshortLinkDO.getShortUri())
                        .enableStatus(hasshortLinkDO.getEnableStatus())
                        .totalPv(hasshortLinkDO.getTotalPv())
                        .totalUv(hasshortLinkDO.getTotalUv())
                        .totalUip(hasshortLinkDO.getTotalUip())
                        .fullShortUrl(hasshortLinkDO.getFullShortUrl())
                        .favicon(Objects.equals(requestParam.getOriginUrl(), hasshortLinkDO.getOriginUrl()) ? hasshortLinkDO.getFavicon() : getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);
                LambdaQueryWrapper<LinkgotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(LinkgotoDO.class)
                        .eq(LinkgotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkgotoDO::getGid, hasshortLinkDO.getGid());
                LinkgotoDO shortLinkGotoDO = linkgotoMapper.selectOne(linkGotoQueryWrapper);
                linkgotoMapper.delete(linkGotoQueryWrapper);
                shortLinkGotoDO.setGid(requestParam.getGid());
                linkgotoMapper.insert(shortLinkGotoDO);
            } finally {
                rlock.unlock();
            }
            }

    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageshortlink(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pagelink(requestParam);
        return  resultPage.convert(each->{
            ShortLinkPageRespDTO respDTO = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            respDTO.setDomain("http://"+respDTO.getDomain());
            return respDTO;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> groupshortlink(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid,count(*) as shortLinkCount ")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String,Object>> shortlinklist=baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortlinklist, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) throws IOException {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl=serverName+serverPort+"/"+shortUri;
        String originalLink=stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));
        if(StrUtil.isNotBlank(originalLink)){
            shortLinkStats(buildLinkStatsRecordAndSetSser(fullShortUrl,request,response));
            ((HttpServletResponse)response).sendRedirect(originalLink);
            return;
        }
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl));
        if(!contains){
            /*跳转空值页面*/
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        String originnullLINK = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl));
        if(StrUtil.isNotBlank(originnullLINK)){
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        /*分布式锁查询数据库*/
        RLock lock = redissonClient.getLock(String.format(GOTO_SHORT_LINK_KEY ,fullShortUrl));
        lock.lock();
        try{
            LambdaQueryWrapper<LinkgotoDO> queryWrapper = Wrappers.lambdaQuery(LinkgotoDO.class)
                    .eq(LinkgotoDO::getFullShortUrl, fullShortUrl);
            LinkgotoDO linkgotoDO = linkgotoMapper.selectOne(queryWrapper);
            if(linkgotoDO==null){
                stringRedisTemplate.opsForValue().set(String.format(LOCK_GOTO_SHORT_LINK_KEY,fullShortUrl),"_",30,TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<ShortLinkDO> queryWrapper1 = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, linkgotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper1);
            if(shortLinkDO==null||(shortLinkDO.getValidDate()!=null&&shortLinkDO.getValidDate().before(new Date()))){
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortUrl),"_",30,TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            stringRedisTemplate.opsForValue().set(
                  String.format(GOTO_SHORT_LINK_KEY+fullShortUrl,fullShortUrl),
                  fullShortUrl,
                  LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),
                  TimeUnit.MILLISECONDS
            );
            shortLinkStats(buildLinkStatsRecordAndSetSser(fullShortUrl,request,response));
            ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
        }finally {
            lock.unlock();
        }

    }

    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO shortLinkStatsRecord) {
        Map<String,String> producerMap=new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(shortLinkStatsRecord));
        shortLinkStatsSaveProducer.send(producerMap);
    }

    public ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetSser(String fullShortUrl,ServletRequest request,ServletResponse response){
        AtomicBoolean uvFirstFlag=new AtomicBoolean();
        Cookie[] cookies=((HttpServletRequest)request).getCookies();
        AtomicReference<String> uv=new AtomicReference<>();
        Runnable addResponseCookieTask=()->{
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60*60*24*30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY+fullShortUrl,uv.get());
        };
        if(ArrayUtil.isNotEmpty(cookies)){
            Arrays.stream(cookies)
                    .filter(each->Objects.equals(each.getName(),"uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each->{
                        uv.set(each);
                        Long uvadd = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvadd!=null&&uvadd>0);
                    },addResponseCookieTask);
        }else {
            addResponseCookieTask.run();
        }
        String remoteAddr=LinkUtil.getActualIp((HttpServletRequest)request);
        String os=LinkUtil.getOs((HttpServletRequest)request);
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipadd = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipfirstflag=uipadd!=null&&uipadd>0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .remoteAddr(remoteAddr)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipfirstflag)
                .browser(browser)
                .device(device)
                .network(network)
                .os(os)
                .currentDate(new Date())
                .build();


    }


    //    生成短链接uri
    public String generator(ShortLinkCreateReqDTO requestParam) {
        int createcount=0;
        String shortUri;
        while (true){
            if(createcount>10){
                throw new ServiceException("短链接生成平凡，请稍后再试");
            }
            String Originalurl=requestParam.getOriginUrl();
            Originalurl+= UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(Originalurl);
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, defaultDomain + "/" + shortUri)
                    .eq(BaseDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if(shortLinkDO==null){
                break;
            }
            createcount++;
        }
        return shortUri;
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}
